HTML5 드래그 앤 드롭 구현
작성일 : 2025.02.07
소개
칸반보드의 핵심 기능인 드래그 앤 드롭을 HTML5의 Drag and Drop API를 사용하여 구현하는 과정을 소개합니다.
1. 드래그 가능한 카드 컴포넌트 구현
카드 컴포넌트에서는 draggable 속성과 dragstart 이벤트를 사용합니다.
const Card = ({ column }: CardProps) => {
const handleDragStart = (e: React.DragEvent) => {
// 드래그 시작할 때 필요한 데이터 저장
e.dataTransfer.setData("columnId", column.id);
e.dataTransfer.setData("fromState", column.state);
};
return (
<div
className="card flex flex-col items-end w-full my-3 p-2
bg-white rounded-md shadow-md shadow-black/40"
draggable
onDragStart={handleDragStart}
>
{/* 카드 내용 */}
</div>
);
};
1.1 드래그 데이터 전달
- dataTransfer 객체를 사용하여 드래그 중인 카드의 정보를 저장
- columnId: 카드의 고유 식별자
- fromState: 카드의 현재 상태(todo, ongoing, completed 등)
2. 드롭 영역(컬럼) 구현
각 상태 컬럼(Todo, Ongoing, Completed)은 드롭 가능한 영역으로 구현됩니다.
const OngoingList = () => {
const curState: StateType = "ongoing";
const { projectBoard, moveCard } = useKanStore();
const dragOverHandler = (e: React.DragEvent) => {
e.preventDefault(); // 드롭을 허용하기 위해 필수
};
const dropHandler = (e: React.DragEvent) => {
e.preventDefault();
// 드래그 중인 카드 정보 가져오기
const columnId = e.dataTransfer.getData("columnId");
const fromState = e.dataTransfer.getData("fromState") as StateType;
// 드롭 위치 계산
const container = e.currentTarget as HTMLElement;
const cards = container.querySelectorAll(".card");
const dropY = e.clientY;
let dropIndex = projectBoard.columns[curState].length;
// 마우스 위치를 기준으로 드롭할 인덱스 계산
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
const rect = card.getBoundingClientRect();
const cardMiddle = rect.top + rect.height / 2;
if (dropY < cardMiddle) {
dropIndex = i;
break;
}
}
// 카드 이동 실행
moveCard(fromState, curState, columnId, dropIndex);
};
return (
<div
className="col-span-1 bg-blue-300 p-2"
onDragOver={dragOverHandler}
onDrop={dropHandler}
>
{/* 컬럼 내용 */}
</div>
);
};
3. 주요 구현 포인트 설명
3.1 드래그 시작 (dragstart)
- draggable 속성을 true로 설정하여 요소를 드래그 가능하게 만듦
- dataTransfer 객체에 필요한 데이터 저장
- 카드의 ID와 현재 상태를 저장하여 이동 시 사용
3.2 드래그 오버 (dragover)
- preventDefault()를 호출하여 드롭을 허용
- 이 처리가 없으면 드롭 이벤트가 발생하지 않음
3.3 드롭 처리 (drop)
-
드롭 위치 계산
- 현재 마우스의 Y 좌표(e.clientY)를 기준
- 각 카드의 위치와 비교하여 삽입할 위치 결정
- getBoundingClientRect()로 카드의 위치 정보 획득
-
카드 이동 실행
- 시작 상태(fromState)와 목표 상태(curState) 확인
- 계산된 인덱스(dropIndex)에 카드 삽입
4. 상 태 관리
이 프로젝트에서는 Zustand를 사용하여 상태를 관리합니다.
const useKanStore = create((set) => ({
projectBoard: {
columns: {
todo: [],
ongoing: [],
completed: []
}
},
moveCard: (fromState, toState, columnId, dropIndex) =>
set((state) => {
// 카드 이동 로직
})
}));
5. 알아두면 좋은 점
-
이벤트 핸들링
- dragstart, dragover, drop 이벤트의 preventDefault() 호출이 중요
- 특히 dragover 이벤트의 preventDefault()가 없으면 드롭이 동작하지 않음
-
드롭 위치 계산
- 마우스 Y 좌표와 카드들의 위치를 비교하여 정확한 삽입 위치 계산
- 카드의 중간 지점을 기준으로 위/아래 결정
-
타입 안정성
- React.DragEvent 타입을 사용하여 타입 안정성 확보
- as 키워드로 필요한 경우 타입 캐스팅
결론
HTML5의 Drag and Drop API를 사용하면 비교적 간단하게 드래그 앤 드롭 기능을 구현할 수 있습니다. 중요한 점은 적절한 이벤트 처리와 정확한 드롭 위치 계산입니다. 이를 통해 사용자에게 직관적이고 자연스러운 UI/UX를 제공할 수 있습니다.